#include <cctype>
#include <algorithm>
#include <sstream>
#include <boost/algorithm/string.hpp>
#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/include/phoenix_core.hpp>
#include <boost/spirit/include/phoenix_operator.hpp>
#include <boost/spirit/include/phoenix_stl.hpp>

#include "src/core/generic/parser.h"
#include "src/core/mvc/controller.h"
#include "tinyxml/tinyxml.h"
#include "src/core/scene/sceneelement.h"

void Parser::parseFile()
{
  logger->setLoggerMark();
  switch (detectFileType())
  {
  case MOL2:
    parseMol2();
    break;
  case ESP:
    parseESP();
    break;
  case Unknown:
    logger->addLogEntry(LoggerRecord(-1, LoggerRecord::FileParsingErrorFiletype, filename));
    sucess = false;
    mol = Molecule();
    break;
  }
  logger->warnUser();
}


Parser::FileType Parser::detectFileType() const
{
  size_t dotPos;
  dotPos = filename.find_last_of('.');
  if (dotPos != std::string::npos)
  {
    std::string suffix = filename.substr(dotPos + 1);
    boost::algorithm::to_lower(suffix);
    if ( !suffix.compare("mol2") ) return MOL2; else
      if ( !suffix.compare("esp") ) return ESP;
  }
  logger->addLogEntry(LoggerRecord(-1, LoggerRecord::FileParsingUnknownFiletype, filename));
  size_t found;
  //checking for mol2
  found = contents.find("@<TRIPOS>");
  if (found != std::string::npos) return MOL2; else
  return Unknown;
}

void Parser::parseMol2()
{
  using boost::spirit::qi::double_;
  using boost::spirit::qi::int_;
  using boost::spirit::qi::char_;
  using boost::spirit::qi::_1;
  using boost::spirit::qi::phrase_parse;
  using boost::spirit::qi::graph;
  using boost::spirit::qi::space;
  using boost::spirit::qi::lexeme;
  using boost::phoenix::ref;


  typedef std::vector<char> string;

  mol = Molecule();
  enum Section {MOLECULE, ATOM, BOND, UNKNOWN} section = UNKNOWN; // unknown sections are skipped
  std::vector<std::string> lines;
  typedef Bond<int> bond_type; // here use atom indexes instead of iterators
  std::vector<bond_type> bonds;

  if (contents.find('\n') != std::string::npos) // lines are ended with \n or \t\n
  {
    boost::algorithm::split(lines, contents, boost::algorithm::is_any_of("\n"));
  } else { // lines are ended with \t
    boost::algorithm::split(lines, contents, boost::algorithm::is_any_of("\t"));
  }

  int secLineCounter = 0;
  int n = lines.size();
  for (int i = 0; i < n; ++i)
  {
    std::string current(lines.at(i));
    boost::algorithm::trim(current);
    if ( current.empty() ) continue;
    if ( current.at(0) == '#' ) continue;
    while ( current.at(current.size() - 1) == '\\')
    {
      ++i;
      current += " " + boost::algorithm::trim_copy(lines.at(i));
    }
    if ( current.at(0) == '@' )
    {
      if ( current.find("@<TRIPOS>MOLECULE") != std::string::npos )
      {
        section = MOLECULE;
        secLineCounter = 0;
      } else if ( current.find("@<TRIPOS>ATOM") != std::string::npos )
      {
        section = ATOM;
        secLineCounter = 0;
      } else if ( current.find("@<TRIPOS>BOND") != std::string::npos )
      {
        section = BOND;
        secLineCounter = 0;
      } else
      {
        section = UNKNOWN;
        secLineCounter = 0;
      }
      continue;
    }
    std::string::iterator first = current.begin(), last = current.end();
    switch (section)
    {
    case MOLECULE:
      {
      switch (secLineCounter)
      {
      case 0: // mol_name
        mol.setName(current);
        break;
      case 1: // num_atoms [num_bonds [num_subst [num_feat [num_sets]]]]
        // skipping this line since now we don't need the data from it
       /* if ( !phrase_parse(current.begin(), current.end(), ( int_[ref(atomN) = _1] >> ( int_[ref(bondN) = _2] >> ( int_ >> ( int_ >> ( int_ | "" ) ) | "" ) | "" ) | "" ), space) )
        {
          logger->addLogMsg(LoggerRecord(LoggerRecord::Warning, QObject::tr("Parsing %1 : line %3 : unexpected syntax. Expected : <code>\"{int} [ {int} [ {int} [ {int} [ {int} ]]]]\"</code>, but got <code>\"%2\"</code> .", "logger record").arg(QString::fromStdString(filename), QString::fromStdString(current)).arg(i)));
        } else {
          if ( atomN <= 0 && atomN != -1 ) logger->addLogMsg(LoggerRecord(LoggerRecord::Warning("Parsing %1 : line %3 : zero or negative atom quantity.").arg(filename, QString::fromStdString(current).toStdString())));
          if ( bondN <= 0 && atomN != -1 ) logger->addLogMsg(LoggerRecord(LoggerRecord::Warning("Parsing %1 : line %3 : zero or negative bond quantity.").arg(filename, QString::fromStdString(current).toStdString())));
        } */
        break;
        // currewntly we do not support any other data from this section
      }
      break;
      }
    case ATOM: // atom_id atom_name x y z atom_type [subst_id [subst_name [charge [status_bit]]]]
      {
      double x = 0.0, y = 0.0, z = 0.0, charge = 0.0;
      int number = 0;
      string v;

      if ( phrase_parse(first, last, ( int_[ref(number) = _1] >> lexeme[+graph] >> double_[ref(x) = _1] >> double_[ref(y) = _1] >> double_[ref(z) = _1] >> lexeme[(+graph)[ref(v) = _1]] >> ( -( int_ >> ( -( lexeme[+(graph)] >> ( -( double_[ref(charge) = _1] >> ( -lexeme[+graph]) ) ) ) ) ) ) ) , space) )
      {
        if ( number <= 0 )
        {
          std::ostringstream os;
          os << filename << "\t" << i;
          logger->addLogEntry(LoggerRecord(-1, LoggerRecord::ParsingBadAtomIndex, os.str() ));
        }
        // it seems that no more checks are possible
        int elem = 0;
        std::string elemstr( std::string(v.begin(), v.end()) );
        if (!elemstr.compare("C.ar")) elem = 6; else
        if (!elemstr.compare("C.2")) elem = 6; else
        if (!elemstr.compare("C.3")) elem = 6; else
        if (!elemstr.compare("H")) elem = 1; else
        if (!elemstr.compare("F")) elem = 9; else
        if (!elemstr.compare("O.2")) elem = 8; else
        if (!elemstr.compare("O.3")) elem = 8; else
        if (!elemstr.compare("N.3")) elem = 7; else
        if (!elemstr.compare("S.3")) elem = 16;  // shulga: todo: make proper binary search through using std::map
        mol.addAtom(Atom(number, elem, Vector3(x, y, z), std::string(v.begin(), v.end()), charge));
      } else {
        std::ostringstream os;
        os << filename << "\t" << i;
        logger->addLogEntry(LoggerRecord(-1, LoggerRecord::ParsingBadAtomEntry, os.str()));
      }
      break;
      }
    case BOND: // bond_id origin_atom_id target_atom_id bond_type [status_bits]
      {
      int number, from, to, type;
      string v;
      if ( phrase_parse(first, last, ( int_[ref(number) = _1] >> int_[ref(from) = _1] >> int_[ref(to) = _1] >> (+graph)[ref(v) = _1] >> -(+graph) ), space ) )
      {
        if (from <= 0 || to <= 0 )
        {
          std::ostringstream os;
          os << filename << "\t" << i;
          logger->addLogEntry(LoggerRecord(-1, LoggerRecord::ParsingBadAtomIndex, os.str()));
        }
        if ( number <= 0 )
        {
          std::ostringstream os;
          os << filename << "\t" << i;
          logger->addLogEntry(LoggerRecord(-1, LoggerRecord::ParsingBadBondIndex, os.str()));
        }
        std::string typestr(v.begin(), v.end());
//        int type;
        if (!typestr.compare("ar")) type = 1; else
//          if (!typestr.compare("1")) type = 1;
        if (!(type = atoi(typestr.c_str())) ) type = 1;
        bonds.push_back(bond_type(number, from, to, type));
      }
      break;
      }
    case UNKNOWN:
      break;
    } // switch (section)
    ++secLineCounter;
  } // for (int i = 0; i < n; i++)

  n = bonds.size();
  for (int i = 0; i < n; ++i)
  {
    bond_type b = bonds.at(i);
    Molecule::atom_iterator newFrom( mol.atomsBegin()), newTo(newFrom);
    bool foundFrom = false, foundTo = false;
    bond_type::int_type from = b.from(), to = b.to();
    for (Molecule::atom_iterator j = mol.atomsBegin(), e = mol.atomsEnd() ; j != e; ++j)
    {
      if ( j->number() == from )
      {
        newFrom = j; foundFrom = true;
        if ( foundTo ) break;
      } else if ( j->number() == to )
      {
        newTo = j; foundTo = true;
        if ( foundFrom ) break;
      }
    }
    if (! (foundFrom && foundTo) )
    {
      std::ostringstream os;
      os << filename << "\t" << b.number() << "\t" << from << "\t" << to;
      logger->addLogEntry(LoggerRecord(-1, LoggerRecord::NoCorrespondingAtom, os.str()));
    } else {
      mol.addBond(Molecule::bond_type(b.number(), Molecule::atom_const_iterator(newFrom), Molecule::atom_const_iterator(newTo), b.order()));
    }
  }
  content = MoleculeContent;
}



void Parser::parseESP()
{
  using boost::spirit::qi::double_;
  using boost::spirit::qi::int_;
  using boost::spirit::qi::_1;
  using boost::spirit::qi::phrase_parse;
  using boost::spirit::qi::space;
  using boost::phoenix::ref;

  const double bohrToAngs = 0.5291772108;

  typedef std::vector<char> string;

  mol = Molecule();
  std::vector<std::string> lines;

  if (contents.find('\n') != std::string::npos) // lines are ended with \n or \t\n
  {
    boost::algorithm::split(lines, contents, boost::algorithm::is_any_of("\n"));
  } else { // lines are ended with \t
    boost::algorithm::split(lines, contents, boost::algorithm::is_any_of("\t"));
  }

  std::string lattice_name = filename;
  lattice_name.append(" 1");
  latt = Lattice(0, lattice_name);
  fiel = Field(0, lattice_name, &latt);
  mol = Molecule(0, lattice_name);

  int points_num;
  double v[4];
  int atom_counter = 0;

  std::string current = lines[0];
  std::string::iterator first = current.begin(), last = current.end();
  if (phrase_parse(first, last, int_ >> int_[ref(points_num) = _1] >> int_, space ) )
  {
    //  header
    latt.reserve(points_num);
    fiel.reserve(points_num);
  }

  for (int i = 1, e = lines.size(); i != e; ++i)
  {
    current = lines[i];
    first = current.begin(), last = current.end();
    if (phrase_parse(first, last, double_[ref(v[0]) = _1] >> double_[ref(v[1]) = _1] >> double_[ref(v[2]) = _1] >> double_[ref(v[3]) = _1], space ) )
    {
      // field point
      latt.addPoint(Lattice::point_type(&(v[1]))*bohrToAngs);
      fiel.addValue(v[0]);
    } else if (phrase_parse(first, last, double_[ref(v[0]) = _1] >> double_[ref(v[1]) = _1] >> double_[ref(v[2]) = _1], space ) )
    {
      // charge_center
      Molecule::atom_type a(atom_counter, 0, Vector3(v)*bohrToAngs);
      atom_counter++;
      mol.addAtom(a);
    }
  }

  content = MoleculeContent | LatticeContent | FieldContent;
}



void parseAtomXml(TiXmlDocument& document, VisitorParameters* properties, Controller* logger = 0)
{
  TiXmlElement* root = document.RootElement();
  if (!root) return;
  if (!root->FirstChild())
  {
    return;
  }
  for( TiXmlElement *element = root->FirstChildElement(); element; element = element->NextSiblingElement() )
  {
    TiXmlHandle elemHandle( element );
    std::string value = element->ValueStr();
    boost::algorithm::to_lower(value);
    if (! value.compare("style"))
    {
      bool error = false;
      TiXmlElement *curElement = elemHandle.FirstChildElement("name").ToElement();
      std::string name;
      if (curElement) name = curElement->GetText(); else
      {
        // reporting an error
        if (logger)
        {
          std::ostringstream os;
          os << document.Value() << "\t" << element->Column() << "\t" << element->Row();
          logger->addLogEntry(LoggerRecord(-1, LoggerRecord::ParsingAtomPropertiesNoStyleNameError, os.str()));
        }
        continue; // moving to next element
      }
      // searching if we already have this style
      unsigned int style = 0;
      for (style = 0; style < properties->styleParameters.size(); ++style )
      {
        if (! properties->styleParameters[style].name.compare(name)) break;
      }
      // creating new style
      if ( style == properties->styleParameters.size() )
      {
        properties->styleParameters.push_back(StyleParameters());
        properties->styleParameters[style].name = name;
      }
      // filling it with parameters
      curElement = elemHandle.FirstChildElement("atoms").FirstChildElement("primitive").ToElement();
      if (curElement)
      {
        int primsCounter = 0;
        for( TiXmlElement *prims = curElement->FirstChildElement(); prims; prims = prims->NextSiblingElement() )
        {
          std::string primName = prims->ValueStr();
          boost::algorithm::to_lower(primName);
          if (! primName.compare("sphere"))
          {
            properties->styleParameters[style].atom_primitives.push_back(Scene3DElement::sphere);
            properties->styleParameters[style].atom_parameters.push_back(std::vector<float>());
          } else if (! primName.compare("cylinder"))
          {
            properties->styleParameters[style].atom_primitives.push_back(Scene3DElement::cylinder);
            properties->styleParameters[style].atom_parameters.push_back(std::vector<float>());
          } else if (! primName.compare("parallelepiped"))
          {
            properties->styleParameters[style].atom_primitives.push_back(Scene3DElement::parallelepiped);
            properties->styleParameters[style].atom_parameters.push_back(std::vector<float>());
          } else if (! primName.compare("point"))
          {
            properties->styleParameters[style].atom_primitives.push_back(Scene3DElement::point);
            properties->styleParameters[style].atom_parameters.push_back(std::vector<float>());
          } else if (! primName.compare("line"))
          {
            properties->styleParameters[style].atom_primitives.push_back(Scene3DElement::line);
            properties->styleParameters[style].atom_parameters.push_back(std::vector<float>());
          } else if (! primName.compare("cone"))
          {
            properties->styleParameters[style].atom_primitives.push_back(Scene3DElement::cone);
            properties->styleParameters[style].atom_parameters.push_back(std::vector<float>());
          } else
          {
            if (logger)
            {
              std::ostringstream os;
              os << document.Value() << "\t" << element->ValueStr() << "\t" << element->Column() << "\t" << element->Row();
              logger->addLogEntry(LoggerRecord(-1, LoggerRecord::ParsingAtomPropertiesUnknownPrimitiveError, os.str()));
              error = true;
              break;
            }
          }
          std::string primParams = prims->GetText();
          std::vector<std::string> lines;
          boost::algorithm::split(lines, primParams, boost::algorithm::is_space());
          for (std::vector<std::string>::const_iterator i = lines.begin(), e = lines.end(); i!=e; ++i)
          {
            if (! i->empty() ) properties->styleParameters[style].atom_parameters[primsCounter].push_back(atof(i->c_str()));
          }
          primsCounter++;
        }
      }
      curElement = elemHandle.FirstChildElement("bonds").FirstChildElement("primitive").ToElement();
      if (curElement)
      {
        int primsCounter = 0;
        for( TiXmlElement *prims = curElement->FirstChildElement(); prims; prims = prims->NextSiblingElement() )
        {
          std::string primName = prims->ValueStr();
          boost::algorithm::to_lower(primName);
          if (! primName.compare("sphere"))
          {
            properties->styleParameters[style].bond_primitives.push_back(Scene3DElement::sphere);
            properties->styleParameters[style].bond_parameters.push_back(std::vector<float>());
          } else if (! primName.compare("cylinder"))
          {
            properties->styleParameters[style].bond_primitives.push_back(Scene3DElement::cylinder);
            properties->styleParameters[style].bond_parameters.push_back(std::vector<float>());
          } else if (! primName.compare("parallelepiped"))
          {
            properties->styleParameters[style].bond_primitives.push_back(Scene3DElement::parallelepiped);
            properties->styleParameters[style].bond_parameters.push_back(std::vector<float>());
          } else if (! primName.compare("point"))
          {
            properties->styleParameters[style].bond_primitives.push_back(Scene3DElement::point);
            properties->styleParameters[style].bond_parameters.push_back(std::vector<float>());
          } else if (! primName.compare("line"))
          {
            properties->styleParameters[style].bond_primitives.push_back(Scene3DElement::line);
            properties->styleParameters[style].bond_parameters.push_back(std::vector<float>());
          } else if (! primName.compare("cone"))
          {
            properties->styleParameters[style].bond_primitives.push_back(Scene3DElement::cone);
            properties->styleParameters[style].bond_parameters.push_back(std::vector<float>());
          } else
          {
            if (logger)
            {
              std::ostringstream os;
              os << document.Value() << "\t" << element->ValueStr() << "\t" << element->Column() << "\t" << element->Row();
              logger->addLogEntry(LoggerRecord(-1, LoggerRecord::ParsingAtomPropertiesUnknownPrimitiveError, os.str()));
              error = true;
              break;
            }
          }
          std::string primParams = prims->GetText();
          std::vector<std::string> lines;
          boost::algorithm::split(lines, primParams, boost::algorithm::is_space());
          for (std::vector<std::string>::const_iterator i = lines.begin(), e = lines.end(); i!=e; ++i)
          {
            if (!i->empty()) properties->styleParameters[style].bond_parameters[primsCounter].push_back(atof(i->c_str()));
          }
          primsCounter++;
        }
      }
    } else if (!value.compare("atom"))
    {
      int atomZ;
      element->Attribute("z", &atomZ);
      if (atomZ >= 120)
      {
        if (logger)
        {
          std::ostringstream os;
          os << document.Value() << "\t" << atomZ << "\t" << element->Column() << "\t" << element->Row();
          logger->addLogEntry(LoggerRecord(-1, LoggerRecord::ParsingAtomPropertiesBadAtomZ, os.str()));
        }
        continue;
      }
      TiXmlElement *radElement = element->FirstChildElement("radius");
      if (radElement)
      {
        properties->atomParameters[atomZ].radius = atof(radElement->GetText());
      }
      TiXmlElement *matElement = element->FirstChildElement("material");
      if (matElement)
      {
        TiXmlElement *ambientElement = matElement->FirstChildElement("ambient");
        TiXmlElement *diffuseElement = matElement->FirstChildElement("diffuse");
        TiXmlElement *specularElement = matElement->FirstChildElement("specular");
        TiXmlElement *shininessElement = matElement->FirstChildElement("shininess");
        if (ambientElement)
        {
          std::vector<std::string> lines;
          std::string text = ambientElement->GetText();
          boost::algorithm::split(lines, text , boost::algorithm::is_space());
          for (std::vector<std::string>::iterator i = lines.begin(); i!= lines.end(); ++i)
          {
            if (i->empty()) { i = lines.erase(i); --i; }
          }
          if (lines.size() == 4)
          {
            float r = atof(lines[0].c_str()), g = atof(lines[1].c_str()), b = atof(lines[2].c_str()), a = atof(lines[3].c_str());
            if ( r < 0 || r > 1.0 || g < 0 || g > 1.0 || b < 0 || b > 1.0 || a < 0 || a > 1.0 )
            {
              if (logger)
              {
                std::ostringstream os;
                os << document.Value() << "\t" << ambientElement->GetText() << "\t" << ambientElement->Column() << "\t" << ambientElement->Row();
                logger->addLogEntry(LoggerRecord(-1, LoggerRecord::ParsingAtomPropertiesBadColorValue, os.str()));
              }
            } else
            properties->atomParameters[atomZ].material.setAmbient(r, g, b, a);
          } else {
            if (logger)
            {
              std::ostringstream os;
              os << document.Value() << "\t" << ambientElement->ValueStr() << "\t" << ambientElement->Column() << "\t" << ambientElement->Row();
              logger->addLogEntry(LoggerRecord(-1, LoggerRecord::ParsingAtomPropertiesBadParameterQuantity, os.str()));
            }
          }
        }

        if (diffuseElement)
        {
          std::vector<std::string> lines;
          std::string text = diffuseElement->GetText();
          boost::algorithm::split(lines, text, boost::algorithm::is_space());
          for (std::vector<std::string>::iterator i = lines.begin(); i!= lines.end(); ++i)
          {
            if (i->empty()) { i = lines.erase(i); --i; }
          }
          if (lines.size() == 4)
          {
            float r = atof(lines[0].c_str()), g = atof(lines[1].c_str()), b = atof(lines[2].c_str()), a = atof(lines[3].c_str());
            if ( r < 0 || r > 1.0 || g < 0 || g > 1.0 || b < 0 || b > 1.0 || a < 0 || a > 1.0 )
            {
              if (logger)
              {
                std::ostringstream os;
                os << document.Value() << "\t" << diffuseElement->GetText() << "\t" << diffuseElement->Column() << "\t" << diffuseElement->Row();
                logger->addLogEntry(LoggerRecord(-1, LoggerRecord::ParsingAtomPropertiesBadColorValue, os.str()));
              }
            } else
            properties->atomParameters[atomZ].material.setDiffuse(r, g, b, a);
          } else {
            if (logger)
            {
              std::ostringstream os;
              os << document.Value() << "\t" << diffuseElement->ValueStr() << "\t" << diffuseElement->Column() << "\t" << diffuseElement->Row();
              logger->addLogEntry(LoggerRecord(-1, LoggerRecord::ParsingAtomPropertiesBadParameterQuantity, os.str()));
            }
          }
        }

        if (specularElement)
        {
          std::vector<std::string> lines;
          std::string text = specularElement->GetText();
          boost::algorithm::split(lines, text, boost::algorithm::is_space());
          for (std::vector<std::string>::iterator i = lines.begin(); i!= lines.end(); ++i)
          {
            if (i->empty()) { i = lines.erase(i); --i; }
          }
          if (lines.size() == 4)
          {
            float r = atof(lines[0].c_str()), g = atof(lines[1].c_str()), b = atof(lines[2].c_str()), a = atof(lines[3].c_str());
            if ( r < 0 || r > 1.0 || g < 0 || g > 1.0 || b < 0 || b > 1.0 || a < 0 || a > 1.0 )
            {
              if (logger)
              {
                std::ostringstream os;
                os << document.Value() << "\t" << specularElement->GetText() << "\t" << specularElement->Column() << "\t" << specularElement->Row();
                logger->addLogEntry(LoggerRecord(-1, LoggerRecord::ParsingAtomPropertiesBadColorValue, os.str()));
              }
            } else
            properties->atomParameters[atomZ].material.setSpecular(r, g, b, a);
          } else {
            if (logger)
            {
              std::ostringstream os;
              os << document.Value() << "\t" << specularElement->ValueStr() << "\t" << specularElement->Column() << "\t" << specularElement->Row();
              logger->addLogEntry(LoggerRecord(-1, LoggerRecord::ParsingAtomPropertiesBadParameterQuantity, os.str()));
            }
          }
        }

        if (shininessElement)
        {
          float sh = atof(shininessElement->GetText());
          properties->atomParameters[atomZ].material.setShininess(sh);
        }
      }
    } else {
      if (logger)
      {
        std::ostringstream os;
        os << document.Value() << "\t" << element->ValueStr() << "\t" << element->Column() << "\t" << element->Row();
        logger->addLogEntry(LoggerRecord(-1, LoggerRecord::ParsingAtomPropertiesUnknownTagError, os.str()));
      }
    }
  }

}

void Parser::parseAtomProperties(std::istream& stream, VisitorParameters* properties)
{
  TiXmlDocument document;
  stream >> document;
  parseAtomXml(document, properties);

}
void Parser::parseAtomProperties(const std::string& filename, VisitorParameters* properties, Controller* logger)
{
  logger->setLoggerMark();
  TiXmlDocument document;
  if (!document.LoadFile(filename))
  {
    std::ostringstream os;
    os << filename << "\t" << document.ErrorDesc() << "\t" << document.ErrorCol() << "\t" << document.ErrorRow();
    logger->addLogEntry(LoggerRecord(-1, LoggerRecord::ParsingAtomPropertiesError, os.str()));

  } else {
    parseAtomXml(document, properties, logger);
  }
  logger->warnUser();
}
